#include "core/main.h"

void GameSnakeBasic::Init()
{
    _spriteBlock       = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/block.png"), "block");
    _spriteBody        = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/body.png"), "body");
    _spriteHeadU       = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/headU.png"), "headU");
    _spriteHeadR       = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/headR.png"), "headR");
    _spriteHeadD       = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/headD.png"), "headD");
    _spriteHeadL       = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/headL.png"), "headL");
    _spriteApple       = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/apple.png"), "apple");
    _spriteGoldenApple = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/goldenApple.png"), "goldenApple");
    _spriteBadApple    = _collection.AddSprite(Bitmap::LoadFile("../assets/snake/dot.png"), "badApple");

    //
    // Font sprites should always be the last thing added (so that it's easiest to ignore them when building feature vectors)
    //
    _collection.AddFontSprites();

    _levelMap.Load(Bitmap::LoadFile("../assets/snake/level04.png"), _spriteBlock);
    
    _observedIntNames.push_back("New Game");
    _observedIntNames.push_back("Score");
    _observedIntNames.push_back("Time Left");
    _observedIntNames.push_back("Direction X");
    _observedIntNames.push_back("Direction Y");
    //observedIntNames.push_back("Snake Length");
    //observedIntNames.push_back("Spawn Frames");

    _activeFrame.observedInts.resize(ObservedIntNames().size());
}

void GameSnakeBasic::NewGame()
{
    _score = 0;
    
    _tiles = _levelMap.tiles;

    Vec2i snakeStart = Vec2i(SnakeConstants::boardWidth / 2, 0);

    _snakeTiles.clear();
    _snakeTiles.push_front(snakeStart);

    _tiles(snakeStart.y, snakeStart.x).spriteIndex = _spriteHeadD;
    _tiles(snakeStart.y, snakeStart.x).type = SnakeTileSelf;
    _snakeDirection = Vec2i(0, 1);

    _maxSnakeLength = 2;

    _advanceFramesLeft = SnakeConstants::frameAdvanceCount;
    _spawnFramesLeft = SnakeConstants::frameAdvanceCount;

    _timeLeft = 60;

    //_framesSinceGameReset = 0;

    ControllerState controller;
    Step(controller);
    Step(controller);

    _activeFrame.observedInts[0] = 1;
}

bool GameSnakeBasic::PassableOrSnakeHead(const SnakeTileEntry &entry) const
{
    return (entry.Passable() || entry.spriteIndex == _spriteHeadU || entry.spriteIndex == _spriteHeadR || entry.spriteIndex == _spriteHeadD || entry.spriteIndex == _spriteHeadL);
}

int GameSnakeBasic::EmptyRadius(const Vec2i &coord) const
{
    const int searchRadius = 4;
    int result = searchRadius + 1;
    for(int yOffset = -searchRadius; yOffset <= searchRadius; yOffset++)
    {
        for(int xOffset = -searchRadius; xOffset <= searchRadius; xOffset++)
        {
            int x = xOffset + coord.x;
            int y = yOffset + coord.y;
            if(_tiles.ValidCoordinates(y, x) && !_tiles(y, x).Passable())
            {
                int dist = Math::AbsInt(xOffset) + Math::AbsInt(yOffset);
                result = Math::Min(dist, result);
            }
        }
    }
    return result;
}

struct DestinationCandidate
{
    Vec2i direction;
    double cost;
};

extern bool operator < (const DestinationCandidate &a, const DestinationCandidate &b);

ControllerState GameSnakeBasic::SimpleAI()
{
    //
    // TODO: deal with 5-frame delay
    //

    vector<GraphEdge> edges;
    for(UINT y = 0; y < SnakeConstants::boardHeight - 1; y++)
    {
        for(UINT x = 0; x < SnakeConstants::boardWidth - 1; x++)
        {
            const SnakeTileEntry &curTile = _tiles(y, x);
            if(PassableOrSnakeHead(curTile) && PassableOrSnakeHead(_tiles(y + 1, x)))
            {
                edges.push_back(GraphEdge(VertexFromCoord(x, y), VertexFromCoord(x, y + 1), 1.0f));
            }
            if(PassableOrSnakeHead(curTile) && PassableOrSnakeHead(_tiles(y, x + 1)))
            {
                edges.push_back(GraphEdge(VertexFromCoord(x, y), VertexFromCoord(x + 1, y), 1.0f));
            }
        }
    }

    Graph g(edges);

    vector<DestinationCandidate> candidates;

    for(UINT y = 1; y < SnakeConstants::boardHeight; y++)
    {
        for(UINT x = 1; x < SnakeConstants::boardWidth; x++)
        {
            const SnakeTileEntry &curTile = _tiles(y, x);

            //if(curTile.Passable() && (Vec2i(x, y) == SnakeConstants::exitCoord || curTile.type == SnakeTileConsumable))
            if(curTile.Passable() && curTile.spriteIndex != _spriteBadApple)
            {
                //vector<UINT> path = g.ShortestPath(VertexFromCoord(_snakeTiles.front()), VertexFromCoord(x, y));
                vector<UINT> path = g.ShortestPath(VertexFromCoord(x, y), VertexFromCoord(_snakeTiles.front()));

                if(path.size() > 0)
                {
                    DestinationCandidate dest;
                    const GraphEdge &curEdge = edges[path[path.size() - 1]];
                    dest.direction = CoordFromVertex(curEdge.GetOtherVertex(VertexFromCoord(_snakeTiles.front()))) - _snakeTiles.front(); //edges(path[0]); CoordFromVertex(path[1]) - CoordFromVertex(path[0]);
                
                    if(Vec2i(x, y) == SnakeConstants::exitCoord)
                    {
                        dest.cost = 0;
                    }
                    else if(curTile.type == SnakeTileConsumable)
                    {
                        dest.cost = path.size();
                    }
                    else if(curTile.type == SnakeTileEmpty)
                    {
                        dest.cost = 10010 - EmptyRadius(Vec2i(x, y));
                    }
                    candidates.push_back(dest);
                }
            }
        }
    }

    sort(candidates.begin(), candidates.end());

    /*sort(candidates.begin(), candidates.end(), 
        [](const DestinationCandidate & a, const DestinationCandidate & b) -> bool
    {
        return a.cost < b.cost; 
    });*/

    ControllerState controller;
    if(rnd() < SnakeConstants::AIStupidity)
    {
        int action = rand() % 4;
        if(action == 0) controller.KeyDown('w');
        if(action == 1) controller.KeyDown('s');
        if(action == 2) controller.KeyDown('a');
        if(action == 3) controller.KeyDown('d');
    }
    else if(candidates.size() > 0)
    {
        if(candidates[0].direction == Vec2i(0, -1)) controller.KeyDown('w');
        if(candidates[0].direction == Vec2i(0,  1)) controller.KeyDown('s');
        if(candidates[0].direction == Vec2i(-1, 0)) controller.KeyDown('a');
        if(candidates[0].direction == Vec2i( 1, 0)) controller.KeyDown('d');
    }
    return controller;
}

void GameSnakeBasic::Step(ControllerState &controller)
{
    if(controller.up()   ) _snakeDirection = Vec2i(0, -1);
    if(controller.down() ) _snakeDirection = Vec2i(0, 1);
    if(controller.left() ) _snakeDirection = Vec2i(-1, 0);
    if(controller.right()) _snakeDirection = Vec2i(1, 0);

    _spawnFramesLeft--;
    if(_spawnFramesLeft == 0)
    {
        _spawnFramesLeft = SnakeConstants::frameAdvanceCount * SnakeConstants::spawnPeriod;

        Vec2i randomTileCoord = Vec2i(rand() % _tiles.Cols(), rand() % _tiles.Rows());
        for(int attempt = 0; attempt < 100; attempt++)
        {
            if(_tiles(randomTileCoord.y, randomTileCoord.x).type != SnakeTileEmpty)
            {
                randomTileCoord = Vec2i(rand() % _tiles.Cols(), rand() % _tiles.Rows());
            }
        }
        if(_tiles(randomTileCoord.y, randomTileCoord.x).type == SnakeTileEmpty)
        {
            _tiles(randomTileCoord.y, randomTileCoord.x).type = SnakeTileConsumable;
            int selection = rand() % 10;
            if(selection <= 1)      _tiles(randomTileCoord.y, randomTileCoord.x).spriteIndex = _spriteGoldenApple;
            else if(selection <= 2) _tiles(randomTileCoord.y, randomTileCoord.x).spriteIndex = _spriteBadApple;
            else                    _tiles(randomTileCoord.y, randomTileCoord.x).spriteIndex = _spriteApple;
        }
    }

    _advanceFramesLeft--;
    if(_advanceFramesLeft == 0)
    {
        _advanceFramesLeft = SnakeConstants::frameAdvanceCount;
        Vec2i nextTileCoord = _snakeTiles.front() + _snakeDirection;

        _timeLeft--;

        bool alive = (_timeLeft > 0) && _tiles.ValidCoordinates(nextTileCoord.y, nextTileCoord.x);
        if(alive)
        {
            const SnakeTileEntry &nextEntry = _tiles(nextTileCoord.y, nextTileCoord.x);
            if(nextEntry.type == SnakeTileWall || nextEntry.type == SnakeTileSelf) alive = false;
            else if(nextEntry.type == SnakeTileConsumable)
            {
                if(nextEntry.spriteIndex == _spriteApple)
                {
                    IncrementScore();
                    _maxSnakeLength += 2;
                    _timeLeft = Math::Min(_timeLeft + 30, 999);
                }
                if(nextEntry.spriteIndex == _spriteGoldenApple)
                {
                    IncrementScore();
                    IncrementScore();
                    _timeLeft = Math::Min(_timeLeft + 30, 999);
                }
                if(nextEntry.spriteIndex == _spriteBadApple)
                {
                    _maxSnakeLength += 4;
                    _timeLeft = Math::Min(_timeLeft + 30, 999);
                }
            }
        }

        if(!alive)
        {
            NewGame();
            return;
        }
        else
        {
            SnakeTileEntry &nextEntry = _tiles(nextTileCoord.y, nextTileCoord.x);
            _tiles(_snakeTiles.front().y, _snakeTiles.front().x).spriteIndex = _spriteBody;

            if(_snakeDirection == Vec2i(0, -1)) nextEntry.spriteIndex = _spriteHeadU;
            if(_snakeDirection == Vec2i(0,  1)) nextEntry.spriteIndex = _spriteHeadD;
            if(_snakeDirection == Vec2i(-1, 0)) nextEntry.spriteIndex = _spriteHeadL;
            if(_snakeDirection == Vec2i( 1, 0)) nextEntry.spriteIndex = _spriteHeadR;
            nextEntry.type = SnakeTileSelf;

            if(int(_snakeTiles.size()) >= _maxSnakeLength)
            {
                SnakeTileEntry &tailEntry = _tiles(_snakeTiles.back().y, _snakeTiles.back().x);
                tailEntry.spriteIndex = -1;
                tailEntry.type = SnakeTileEmpty;
                _snakeTiles.pop_back();
            }
            _snakeTiles.push_front(nextTileCoord);
        }
    }

    //_framesSinceGameReset++;
    //if(_framesSinceGameReset == 2) _activeFrame.observedInts[0] = 1; else _activeFrame.observedInts[0] = 0;
    _activeFrame.observedInts[0] = 0;
    _activeFrame.observedInts[1] = _score;
    _activeFrame.observedInts[2] = _timeLeft;
    _activeFrame.observedInts[3] = _snakeDirection.x;
    _activeFrame.observedInts[4] = _snakeDirection.y;
    //_activeFrame.observedInts[8] = _maxSnakeLength;
    //_activeFrame.observedInts[9] = _spawnFramesLeft;

    MakeFrameSprites();
}

void GameSnakeBasic::IncrementScore()
{
    _score++;
}

void GameSnakeBasic::MakeFrameSprites()
{
    _activeFrame.sprites.clear();
    for(UINT y = 0; y < SnakeConstants::boardHeight; y++)
    {
        for(UINT x = 0; x < SnakeConstants::boardWidth; x++)
        {
            const SnakeTileEntry &curTile = _tiles(y, x);
            if(curTile.spriteIndex != -1)
            {
                _activeFrame.sprites.push_back(SpriteLocation(Vec2i(x * 8, y * 8), curTile.spriteIndex, 0));
            }
        }
    }

    _collection.DrawText("SCORE", Vec2i(SnakeConstants::boardWidth + 1, 2), _activeFrame.sprites, 0);
    _collection.DrawNumber(_score, 6, Vec2i(SnakeConstants::boardWidth + 1, 3), _activeFrame.sprites, 0);

    _collection.DrawText("TIME", Vec2i(SnakeConstants::boardWidth + 1, 5), _activeFrame.sprites, 0);
    _collection.DrawNumber(_timeLeft, 3, Vec2i(SnakeConstants::boardWidth + 1, 6), _activeFrame.sprites, 0);
}
